package tester.utilities;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

import tester.*;

/**
 * 
 * @author Weston Jossey
 *
 */
public class TestRunner implements Runnable{
	int errors;
	int numberOftestsRun;
	boolean failed;

	Object exampleObject;
	Class<?> exampleClass;
	Tester tester;

	Method[] allMethods;
	Collection<Method> testMethods;
	Collection<Method> classSetupMethods;
	Collection<Method> methodsBeforeTests;
	Collection<Method> methodsAfterTests;
	Collection<Method> classTearDownMethods;

	public TestRunner(Object example, Tester t) {
		this.exampleObject = example;
		this.exampleClass = example.getClass();
		this.tester = t;
		this.failed = false;
		this.errors = 0;
		this.numberOftestsRun = 0;

		/* Init */
		constructSequence();
	}

	/**
	 * Returns the number of errors encountered by the TestRunner
	 * @return number of errors
	 */
	public int getErrors() {
		return this.errors;
	}

	/**
	 * Number of tests that were executed by
	 * the TestRunner
	 * @return number of tests run
	 */
	public int numberOfTestsRun() {
		return numberOftestsRun;
	}

	/**
	 * Returns a boolean that represents whether or not the TestRunner
	 * failed or succeeded.  
	 * @return returns true if succeeded, otherwise false
	 */
	public boolean failed() {
		return this.failed;
	}

	/**
	 * Constructs the necessary objects so that the tests can execute
	 */
	private void constructSequence() {

		/* Collect the methods */
		this.testMethods = findMethodsWithAnnotation(exampleClass, TestMethod.class);
		this.classSetupMethods = findMethodsWithAnnotation(exampleClass,
				ClassSetup.class);
		this.methodsBeforeTests = findMethodsWithAnnotation(exampleClass,
				BeforeTests.class);
		this.methodsAfterTests = findMethodsWithAnnotation(exampleClass,
				AfterTests.class);
		this.classTearDownMethods = findMethodsWithAnnotation(exampleClass,
				ClassTearDown.class);

		/* Sort the methods */
		if (this.testMethods != null) {
			this.testMethods = DependencyArranger
					.sortByDependencies(this.testMethods);
		}
		if (this.classSetupMethods != null) {
			this.classSetupMethods = DependencyArranger
					.sortByDependencies(this.classSetupMethods);
		}
		if (this.methodsBeforeTests != null) {
			this.methodsBeforeTests = DependencyArranger
					.sortByDependencies(this.methodsBeforeTests);
		}
		if (this.methodsAfterTests != null) {
			this.methodsAfterTests = DependencyArranger
					.sortByDependencies(this.methodsAfterTests);
		}
		if (this.classTearDownMethods != null) {
			this.classTearDownMethods = DependencyArranger
					.sortByDependencies(this.classTearDownMethods);
		}

	}

	/**
	 * Run the tests
	 */
	public void run() {
		runAndExecute();
	}

	/**
	 * Invokes the given method with the given arguments
	 * @param m method to invoke
	 * @param args arguments to invoke the method with
	 */
	private void invoke(Method m, Object... args){
		try {
			m.invoke(exampleObject, args);
		} catch (Throwable e) {
			System.err.println("Unable to invoke method.  "
					+ "Make sure the method has proper arguments");
			e.printStackTrace();
		}
	}
	
	/**
	 * Invokes a collection of methods with the given preTest and post
	 * test methods
	 * @param methodsToInvoke methods to invoke
	 * @param preTestList methods to invoke before each test
	 * @param postTestList methods to invoke after each test
	 */
	private void invokeMethods(Collection<Method> methodsToInvoke,
			Collection<Method> preTestList,
			Collection<Method> postTestList) {
		
		if(preTestList == null && postTestList == null){
			if(methodsToInvoke != null){
				for(Method method : methodsToInvoke){
					invoke(method, this.tester);
					this.numberOftestsRun++;
				}
			}
		}else if(methodsToInvoke != null){
			for (Method testMethod : methodsToInvoke) {
				if (testMethod != null) {
					try{
						//Invoke pre-methods
						if(preTestList != null){
							for(Method beforeMethod : preTestList){
								invoke(beforeMethod);
							}
						}
						
						//Invoke method
						invoke(testMethod, this.tester);
						this.numberOftestsRun++;
						
						//Invoke post-methods
						if(postTestList != null){
							for(Method afterMethod : postTestList){
								invoke(afterMethod);
							}
						}
					}catch (Throwable e) {
						this.errors = this.errors + 1;
						System.out.println("Threw exception during test "
								+ this.numberOftestsRun);
						e.printStackTrace();
						failed = true;
					}// print the test results at the end
				}
			}
		}
		
	}
	
	private void invokeClassSetupOrTeardown(Collection<Method> methodList){
		if(methodList != null){
			for(Method m : methodList){
				invoke(m, new Object[0]);
			}
		}
	}

	/**
	 * Runs all of tests in the proper order
	 */
	private void runAndExecute() {
		// make sure there are tests to run
		if (testMethods != null) {

			/* Run all of the class-wide setup methods */

			invokeClassSetupOrTeardown(classSetupMethods);
			
			invokeMethods(testMethods, methodsBeforeTests, methodsAfterTests);
			
			invokeClassSetupOrTeardown(classTearDownMethods);
		}
	}

	/**
	 * Find all of the methods for the given class with the matching 
	 * annotation
	 * @param example class to find methods within
	 * @param annotationClass annotation class to match
	 * @return returns a collection with all the methods
	 */
	private Collection<Method> findMethodsWithAnnotation(Class<?> example,
			Class<? extends Annotation> annotationClass) {

		if (allMethods == null) {
			allMethods = example.getDeclaredMethods();
		}

		ArrayList<Method> foundMethods = new ArrayList<Method>();

		// make a list of all methods with the given name
		for (Method method : allMethods) {

			if (method.isAnnotationPresent(annotationClass)) {
				foundMethods.add(method);
				method.setAccessible(true);
			}
		}

		if (foundMethods.size() > 0) {
			// found test methods that matched the given parameter list
			return foundMethods;
		}
		// no test methods that matched the given parameter list
		else {
			return null;
		}

	}

}
